package org.elanfox.cloud.mybatis.utils;

import cn.hutool.core.util.ClassUtil;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.elanfox.cloud.mybatis.base.Base2PO;
import org.elanfox.cloud.mybatis.base.BasePO;
import org.elanfox.cloud.mybatis.constant.JavaToMysqlType;
import org.elanfox.cloud.mybatis.constant.MysqlTypeConstant;
import org.elanfox.cloud.mybatis.ddl.model.EntityColumn;
import org.elanfox.cloud.mybatis.ddl.model.EntityTable;
import tk.mybatis.mapper.annotation.ColumnType;
import tk.mybatis.mapper.annotation.KeySql;

import javax.persistence.*;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author allan
 * @version 1.0.0
 * @date 2021/4/20
 */
@Slf4j
public class EntityHelper {
    private static Map<Class<?>, EntityTable> ENTITY_TABLES = new ConcurrentHashMap<>();

    public static Map<Class<?>, EntityTable> getEntityTables() {
        return ENTITY_TABLES;
    }

    public static void init(String[] scanPackages) throws NoSuchFieldException {
        for (String scanPackage : scanPackages) {
            Set<Class<?>> classes = ClassUtil.scanPackage(scanPackage);
            for (Class<?> aClass : classes) {
                resolveEntity(aClass);
            }
        }
    }

    @AllArgsConstructor
    private static class TwoTuple<T> {
        private T one;
        private T two;
    }

    private static void resolveEntity(Class<?> entityClass) throws NoSuchFieldException {
        EntityTable entityTable = new EntityTable();
        Entity entity = entityClass.getAnnotation(Entity.class);
        if (entity == null) {
            log.warn("Class named " + entityClass.getName() + " has no @Entity, Please set annotation with @Entity");
        }
        Table table = entityClass.getAnnotation(Table.class);
        if (table == null) {
            log.warn("entity class named " + entityClass.getName() + " has no @Table, ignore");
            return;
        }
        entityTable.setName(table.name());
        //设置column
        List<EntityColumn> columns = new ArrayList<>();
        List<EntityColumn> pkColumns = new ArrayList<>();
        Field[] fields = entityClass.getDeclaredFields();

        for (Field field : fields) {
            //如果有@Transient，则略过
            if (field.getAnnotation(Transient.class) != null) {
                continue;
            }

            EntityColumn entityColumn = resolveField(field, entityClass);
            columns.add(entityColumn);
            if (entityColumn.isKey()) {
                pkColumns.add(entityColumn);
            }
        }
        if (Base2PO.class.isAssignableFrom(entityClass)) {
            Field[] f = Base2PO.class.getDeclaredFields();
            for (Field field : f) {
                EntityColumn entityColumn = resolveField(field, entityClass);
                columns.add(entityColumn);
            }
        }
        if (BasePO.class.isAssignableFrom(entityClass)) {
            Field[] f = BasePO.class.getDeclaredFields();
            for (Field field : f) {
                EntityColumn entityColumn = resolveField(field, entityClass);
                columns.add(entityColumn);
            }
        }

        entityTable.setEntityColumns(columns);
        entityTable.setPkColumns(pkColumns);

        ENTITY_TABLES.put(entityClass, entityTable);
    }

    private static EntityColumn resolveField(Field field, Class<?> entityClass) {
        EntityColumn entityColumn = resolveColumn(entityClass, field);

        //处理主键
        resolvePKColumn(field, entityColumn);

        return entityColumn;

    }

    private static TwoTuple<String> resolveType(Column column, Field field, Class<?> entityClass) {
        String jdbcType;
        String dataType;

        if (StringUtils.isEmpty(column.columnDefinition())) {
            MysqlTypeConstant type = null;

            ColumnType columnType = field.getAnnotation(ColumnType.class);
            //关联实体
            if (columnType != null) {
                Field[] joinFields = field.getType().getDeclaredFields();
                for (Field joinField : joinFields) {
                    Id id = joinField.getAnnotation(Id.class);
                    if (id != null) {
                        type = JavaToMysqlType.javaToMysqlTypeMap.get(joinField.getGenericType().toString());
                        break;
                    }
                }
            } else {
                type = JavaToMysqlType.javaToMysqlTypeMap.get(field.getGenericType().toString());
            }
            //如果是枚举
            if (field.getType().isEnum()) {
                type = MysqlTypeConstant.TINYINT;
            }

            if (type == null) {
                throw new RuntimeException("In Entity " + entityClass.getName() + ",field " + field.getName() + " can not be cast to jdbc type");
            }

            jdbcType = type.name();
            dataType = type.name();
            if (type == MysqlTypeConstant.VARCHAR) {
                jdbcType += "(" + column.length() + ")";
            }
            if (type == MysqlTypeConstant.DECIMAL) {
                if (column.precision() > 0) {
                    jdbcType += "(" + column.precision() + ")";
                    if (column.scale() > 0) {
                        jdbcType = type.name() + "(" + column.precision() + "," + column.scale() + ")";
                    }
                }
            }
            if (type == MysqlTypeConstant.BIGINT || type == MysqlTypeConstant.TINYINT || type == MysqlTypeConstant.INT) {
                if (column.precision() > 0) {
                    jdbcType += "(" + column.precision() + ")";
                } else {
                    jdbcType += "(" + defaultLength(type) + ")";
                }
            }
            if (type == MysqlTypeConstant.BIT) {
                jdbcType += "(1)";
            }
            Lob lob = field.getAnnotation(Lob.class);
            if (lob != null) {
                jdbcType = MysqlTypeConstant.TEXT.name();
            }
        } else {
            jdbcType = column.columnDefinition();
            dataType = jdbcType.split("\\(")[0];
        }

        return new TwoTuple<>(jdbcType, dataType);
    }

    private static Column getColumn(Field field, Class<?> entityClass) {
        Column column = field.getAnnotation(Column.class);
        if (column == null) {
            throw new RuntimeException("In Entity " +
                    entityClass.getName() + ",field " +
                    field.getName() + " has no @Column annotation, " +
                    "or use @Transient to ignore ");
        }
        return column;
    }

    @SneakyThrows
    private static EntityColumn resolveColumn(Class<?> entityClass, Field field) {
        EntityColumn entityColumn = new EntityColumn();

        if (field.isAnnotationPresent(JoinColumn.class)) {
            JoinColumn joinColumn = field.getAnnotation(JoinColumn.class);

            entityColumn.setName(joinColumn.name());
            entityColumn.setUnique(joinColumn.unique());
            entityColumn.setNullable(joinColumn.nullable());

            String jdbcType = null;
            String dataType = null;
            if (StringUtils.isEmpty(joinColumn.columnDefinition())) {
                if (StringUtils.isEmpty(joinColumn.referencedColumnName())) {
                    //找主键
                    Field[] joinFields = field.getType().getDeclaredFields();
                    for (Field joinField : joinFields) {
                        if (joinField.isAnnotationPresent(Id.class)) {
                            Column column = getColumn(joinField, field.getType());
                            TwoTuple<String> type = resolveType(column, joinField, field.getType());
                            jdbcType = type.one;
                            dataType = type.two;
                            break;
                        }
                    }
                } else {
                    //找对应的
                    Field joinField = field.getType().getDeclaredField(joinColumn.referencedColumnName());
                    Column column = getColumn(joinField, field.getType());
                    TwoTuple<String> type = resolveType(column, joinField, field.getType());
                    jdbcType = type.one;
                    dataType = type.two;
                }
            } else {
                jdbcType = joinColumn.columnDefinition();
                dataType = jdbcType.split("\\(")[0];
            }
            entityColumn.setJdbcType(jdbcType);
            entityColumn.setDataType(dataType);
        } else {
            Column column = getColumn(field, entityClass);

            entityColumn.setName(column.name());
            entityColumn.setUnique(column.unique());
            entityColumn.setNullable(column.nullable());
            TwoTuple<String> type = resolveType(column, field, entityClass);

            entityColumn.setJdbcType(type.one);
            entityColumn.setDataType(type.two);
        }

        return entityColumn;
    }

    private static void resolvePKColumn(Field field, EntityColumn entityColumn) {
        Id id = field.getAnnotation(Id.class);
        if (id != null) {
            entityColumn.setKey(true);
            KeySql keySql = field.getAnnotation(KeySql.class);
            if (keySql != null && keySql.useGeneratedKeys()) {
                entityColumn.setAutoIncrement(true);
            }
            entityColumn.setNullable(false);
        }
    }

    public static void release() {
        ENTITY_TABLES = null;
    }

    private static int defaultLength(MysqlTypeConstant type) {
        switch (type) {
            case INT:
                return 11;
            case BIGINT:
                return 20;
            case TINYINT:
                return 4;
            default:
                return 0;
        }
    }
}
